import { createProxyByInvokeMethod } from '../proxy'
import { FastWorkerOptions } from '../types'
import { once } from '../utils'
import { HookFn, initWorkersPool, WorkerPool } from '../workers-pool'

const getDispose = (pool: WorkerPool, onInit?: Function, onCleanup?: Function, onlyExecInWorker?: boolean) =>
  once(() => {
    if (onInit) {
      pool.offInit(onInit as any)
    }
    if (onCleanup) {
      pool.every(onCleanup as any, onlyExecInWorker)
    }
    pool.unref()
  })

const registerWorkerFinalization: (
  fastWorker: FastWorkerRaw<any>,
  pool: WorkerPool,
  onInit?: Function,
  onCleanup?: Function,
  onlyExecInWorker?: boolean
) => () => void =
  typeof FinalizationRegistry !== 'undefined'
    ? (() => {
        const finalizationRegistry = new FinalizationRegistry<() => void>(dispose => {
          dispose()
        })

        return (fastWorker, pool, onInit, onCleanup?: Function, onlyExecInWorker?: boolean) => {
          const dispose = getDispose(pool, onInit, onCleanup, onlyExecInWorker)
          finalizationRegistry.register(fastWorker, dispose)
          return dispose
        }
      })()
    : (fastWorker, pool, onInit, onCleanup?: Function, onlyExecInWorker?: boolean) =>
        getDispose(pool, onInit, onCleanup, onlyExecInWorker)

export class FastWorkerRaw<Module extends Record<keyof any, any>> {
  methods
  private _p
  private _m
  options: FastWorkerOptions<Module>
  dispose: () => void

  constructor(modulePath: string, options?: FastWorkerOptions<Module>) {
    const pool = initWorkersPool()
    this.methods = Object.create(null)
    this._p = pool
    this._m = modulePath
    this.options = options ?? (options = Object.create(null))

    pool.ref()

    const onInit = options?.onInit
    const onCleanup = options?.onCleanup

    let onInitProxy: HookFn | undefined
    let cleanupProxy: HookFn | undefined

    if (typeof onInit === 'function') {
      onInitProxy = (invokeMethod, isMainThread) => {
        const doInvokeMethod = (method: string, args: any[]) => {
          return invokeMethod(this._m, method, args, options!.timeout, options!.maxRetries || 0, options!.retryInterval)
        }

        const worker = createProxyByInvokeMethod(doInvokeMethod)
        onInit.call(worker, worker, isMainThread)
      }

      pool.onInit(onInitProxy, options!.onlyExecInWorker)
    }

    if (typeof onCleanup === 'function') {
      cleanupProxy = (invokeMethod, isMainThread) => {
        const doInvokeMethod = (method: string, args: any[]) => {
          return invokeMethod(this._m, method, args, options!.timeout, options!.maxRetries || 0, options!.retryInterval)
        }

        const worker = createProxyByInvokeMethod(doInvokeMethod)
        onCleanup.call(worker, worker, isMainThread)
      }
    }

    const dispose = registerWorkerFinalization(this, pool, onInitProxy, cleanupProxy, options?.onlyExecInWorker)
    this.dispose = () => {
      dispose()
      this._p = undefined!
    }
  }

  invokeMethod(method: string, args: any[]) {
    if (!this._p) {
      throw new Error('This instance of FastWorker is disposed.')
    }
    const { options } = this
    return this._p.invokeMethod(
      this._m,
      method,
      args,
      options.timeout,
      options.maxRetries || 0,
      options.retryInterval,
      options.onlyExecInWorker
    )
  }

  getMethod(method: string) {
    return this.methods[method] ?? (this.methods[method] = (...args: any[]) => this.invokeMethod(method, args))
  }

  get disposed() {
    return !this._p
  }
}
